// @vitest-environment jsdom // // `/[slug]` per-release page — `getStaticPaths` (one path per release), // `loader` (look up the release, shiki-highlight it, `notFound()` on a miss), // `meta(loaderData)`, and the render (which injects the highlighted HTML). We // mock `../lib/releases` (avoids the node-only glob) and `@voltro/changelog` // (`highlightRelease` pulls in shiki) and `@voltro/web` (`notFound`). import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { act, createElement, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' // Type-only import — erased at runtime, so it does NOT pull @voltro/changelog's // node-only glob (the value module stays mocked below). Using the real Release // type keeps the mock in lockstep with the framework's shape (tags: ReleaseTag[], // draft, body, …) so the test typechecks against what the page actually consumes. import type { Release } from '@voltro/changelog' const releases: ReadonlyArray = [ { version: '0.1.0', releasedAt: '2026-01-15', slug: 'v0-1-0', tags: ['feature'], title: 'First release', summary: 'The initial cut.', draft: false, body: 'The initial cut.', html: '

plain

', }, ] const releaseBySlug = (slug: string): Release | undefined => releases.find((r) => r.slug === slug) // `../../lib/releases` — the path the PAGE imports. This read `../lib/releases`, // which from `src/pages/[slug]/` resolves to `src/pages/lib/releases`: a module // that does not exist, so the mock registered against nothing and the REAL // `src/lib/releases.ts` loaded. A wrong `vi.mock` specifier is silent in vitest // — it fails later and elsewhere, here as "No `loadReleases` export is defined // on the `@voltro/changelog` mock", because the real module calls it at import // time. The sibling `[locale]/mirrors.test.tsx`, at the same depth, had it right. vi.mock('../../lib/releases', () => ({ releases, releaseBySlug })) // `highlightRelease` runs shiki in node; the page only needs it to return a // (possibly re-rendered) release — the identity, tagged so we can assert it ran. vi.mock('@voltro/changelog', () => ({ highlightRelease: async (r: Release) => ({ ...r, html: `
${r.title}
` }), })) const loaderData = vi.fn<() => { release: Release }>() class NotFoundError extends Error {} vi.mock('@voltro/web', () => ({ useLoaderData: () => loaderData(), notFound: (detail?: string) => { throw new NotFoundError(detail) }, // `withLocalePrefix` (lib/locale) reads useLocation; the page pins the locale // via useLocale, so any bare path is fine here. useLocation: () => '/', })) // The back-link chrome comes from @voltro/i18n; stub it (useLocale → en, // passes children through). vi.mock('@voltro/i18n', () => ({ defineCatalog: (c: T): T => c, defineLocale: () => (c: T): T => c, useLocale: () => 'en', T: ({ id }: { id: string }) => id, })) const { default: ReleasePage, getStaticPaths, loader, meta } = await import('./page') ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true let container: HTMLDivElement let root: Root const render = (node: ReactNode): void => { container = document.createElement('div') document.body.appendChild(container) act(() => { root = createRoot(container) root.render(node) }) } beforeEach(() => { loaderData.mockReset() }) afterEach(() => { if (root) act(() => root.unmount()) container?.remove() document.body.innerHTML = '' }) describe('changelog [slug] — getStaticPaths', () => { test('enumerates one path per release slug', async () => { const paths = await getStaticPaths() expect(paths.map((p) => p.params.slug)).toEqual(['v0-1-0']) }) }) describe('changelog [slug] — loader', () => { test('resolves the release and runs it through highlightRelease', async () => { const { release } = await loader({ params: { slug: 'v0-1-0' } } as never) expect(release.slug).toBe('v0-1-0') expect(release.html).toBe('
First release
') }) test('throws notFound for an unknown slug', async () => { await expect(loader({ params: { slug: 'nope' } } as never)).rejects.toBeInstanceOf( NotFoundError, ) }) }) describe('changelog [slug] — meta', () => { test('builds the per-release title + description from the loader data', () => { const m = meta({ loaderData: { release: releases[0]! } }) expect(m.title).toContain('First release') expect(m.description).toBe('The initial cut.') }) }) describe('changelog [slug] — render', () => { test('renders the title, version, and injects the highlighted HTML body', () => { loaderData.mockReturnValue({ release: { ...releases[0]!, html: '

the body html

' } }) render(createElement(ReleasePage)) expect(container.querySelector('h1')?.textContent).toBe('First release') expect(container.textContent).toContain('v0.1.0') expect(container.innerHTML).toContain('the body html') }) })